﻿using System;

using System.Collections.Generic;
using System.Text;
using Silvermoon.Controls.Base;
using System.Drawing;
using Silvermoon.Animations;
using System.Diagnostics;
using Silvermoon.UI;
using Silvermoon.Windows;
using Silvermoon.Controls.Interfaces;

namespace Silvermoon.Controls
{
    public abstract class NavigationBase : CompositeControlBase, ISynchronizable
    {
        #region classes

        protected class Tile : Control, IDecorator
        {
            protected readonly ScaleTransform ScaleTransform = new ScaleTransform();
            private ControlCollection controls = new ControlCollection(null);

            public INavigationNode Node;

            /// <summary>
            /// Required to determine what node relative to the selected node is taped. previous nodes have a negative value, next nodes a positive, while 0 means selected node.
            /// Note that this value must be set during layout!
            /// </summary>
            public int Delta;

            public Tile()
                : base()
            {
                Transformation = ScaleTransform;
            }


            protected override void Dispose(bool dispose)
            {
                Control = null;
                base.Dispose(dispose);
            }



            public float ScaleFactor
            {
                get { return ScaleTransform.X; }
                set
                {
                    if (ScaleTransform.X != value)
                    {
                        ScaleTransform.X = ScaleTransform.Y = value;
                        Invalidate(InvalidationFlag.Transformation);
                    }
                }
            }

            #region IDecorator Members

            public Control Control
            {
                get { return controls.First; }
                set
                {
                    if (controls.First != value)
                    {
                        controls.First = value;
                        if (value != null) value.Parent = this;
                        Invalidate(InvalidationFlag.Layout);
                    }
                }
            }

            protected internal override ControlCollection GetVisibleControls()
            {
                return controls;
            }

            protected internal override void OnTap(TapEventArgs e)
            {
                if (!e.Handled && Control != null)
                {
                    NavigationBase parent = Parent as NavigationBase;
                    if (parent != null)
                    {
                        parent.OnTileTap(this);
                        e.Handled = true;
                    }
                }
            }


            #endregion

            protected override void OnLayout()
            {
                if (CheckFlag(InvalidationFlag.Size | InvalidationFlag.Layout) && Control != null)
                {
                    Control c = Control;
                    Margin m = c.Margin;
                    c.Bounds = m.Layout(c.Bounds, Size);
                }
            }

            protected internal override void OnRender(Silvermoon.Core.Renderer renderer)
            {
                if (Control != null) renderer.RenderControl(Control);
            }


            protected internal override void OnTouchDown(TouchEventArgs e)
            {
                if (!e.Handled) SetState(VState.Pressed);
                base.OnTouchDown(e);
            }

            protected internal override void OnTouchUp(TouchEventArgs e)
            {
                SetState(VState.Normal);
                base.OnTouchUp(e);
            }

        }

        #endregion
        #region fields

        protected const int SCROLLING = 1;

        private bool allowFlick;
        private int scrollDuration = 350;
        protected ContainerGenerator<Control, INavigationNode> containerGenerator;
        protected INavigationNode selectedNode;
        protected float offset;
        protected int bitmask;
        private IEasingFunction scrollEase = PowerEase.CubicOut;

        #endregion
        #region accessors

        public static PropertyAccessor<NavigationBase, float> OffsetProperty = PropertyAccessor.Register<NavigationBase, float>("Offset", (c) => (c).Offset, (c, v) => (c).Offset = v);

        #endregion
        #region ctors
        public NavigationBase()
            : base()
        {
            containerGenerator = new ContainerGenerator<Control, INavigationNode>(this.OnGetControl, this.OnReleaseControl);
            AllowDrag = true;
            ControlFlags |= ControlFlags.CanPan;
        }

        protected override void Dispose(bool dispose)
        {
            base.Dispose(dispose);
            containerGenerator.Dispose();
        }

        #endregion
        #region props

        public IEasingFunction ScrollEase
        {
            get { return scrollEase; }
            set { scrollEase = value; }
        }

        /// <summary>
        /// Gets or sets the duration in ms to scroll from one node to another.
        /// </summary>
        public int ScrollDuration
        {
            get { return scrollDuration; }
            set
            {
                scrollDuration = Math.Max(0, value);
            }
        }

        /// <summary>
        /// Gets or sets whether navigation by dragging is allowed.
        /// </summary>
        public bool AllowDrag
        {
            get;
            set;
        }

        public bool IsScrolling
        {
            get { return (bitmask & SCROLLING) != 0; }
            set
            {
                if (IsScrolling != value)
                {
                    bitmask = value ? bitmask | SCROLLING : bitmask & ~SCROLLING;
                    OnScrollingChanged();
                }
            }
        }

        /// <summary>
        /// Gets or sets the offset that indicates how to render the pivot.
        /// A value of 0f indicates to render the current control, a value of -1f the previous and a value of 1f the next.
        /// Values outside of -1f..1f are clipped.
        /// </summary>
        public float Offset
        {
            get { return offset; }
            set
            {
                if (offset != value)
                {
                    offset = value;
                    Invalidate(InvalidationFlag.Appearance);
                    OnOffsetChanged();
                }
            }
        }

        /// <summary>
        /// Gets or sets the selected node.
        /// </summary>
        public INavigationNode SelectedNode
        {
            get { return selectedNode; }
            set
            {
                if (selectedNode != value)
                {
                    var old = selectedNode;
                    selectedNode = value;
                    OnSelectedNodeChanged(old);
                }
            }
        }

        /// <summary>
        /// Gets the current control.
        /// </summary>
        public Control Control
        {
            get { return selectedNode != null ? containerGenerator.GetContainerFromData(selectedNode) : null; }
        }

        private int ScrollWidth
        {
            get { return Width; }
        }

        #endregion
        #region Methods

        /// <summary>
        /// Occurs when <see cref="IsScrolling"/> value changes.
        /// </summary>
        protected virtual void OnScrollingChanged()
        {
            RaiseEvent(ScrollingChangedEvent);
        }

        protected INavigationNode GetFocusedNode()
        {
            INavigationNode node = this.selectedNode;
            float offset = this.offset;
            while (offset >= 1f)
            {
                node = node != null ? node.Previous : null;
                offset -= 1f;
            }
            while (offset <= -1f)
            {
                node = node != null ? node.Next : null;
                offset += 1f;
            }
            return node;
        }

        /// <summary>
        /// Gets the control that displays the value of a <see cref="INavigationNode"/>.
        /// </summary>
        /// <param name="node">INavigationNode that contains the value to display.</param>
        /// <returns>Control that displays the value of the specified node.</returns>
        protected virtual Control OnGetControl(INavigationNode node)
        {
            if (node == null) return null;
            if (GetControl != null)
            {
                NavigationEventArgs e = new NavigationEventArgs(node);
                GetControl(this, e);
                return e.Control;
            }
            else
            {
                Control c = node as Control;
                if (c == null) throw new NotImplementedException("No GetControl event handler or OnGetControl overload.");
                return c;
            }
        }

        /// <summary>
        /// Releases a control that displays a specific node.
        /// </summary>
        /// <param name="node">INavigationNode that containts the data.</param>
        /// <param name="control">Control that displays the value of the node.</param>
        /// <returns>Null if the control is disposed, otherwise control.</returns>
        /// <example>
        /// A method could dispose the control that was created to display the specified node.
        /// </example>
        protected virtual Control OnReleaseControl(INavigationNode node, Control control)
        {
            if (control == null || node == null) return null;
            control.RemoveFromParent();
            if (ReleaseControl != null)
            {
                NavigationEventArgs e = new NavigationEventArgs(node);
                e.Control = control;
                ReleaseControl(this, e);
                return e.Control;
            }
            else return null;
        }

        /// <summary>
        /// Occurs when the offset has changed.
        /// </summary>
        protected virtual void OnOffsetChanged()
        {
            if (OffsetChanged != null) OffsetChanged(this, EventArgs.Empty);
        }

        public void SynchronizeWith(ISynchronizable control)
        {
            if (control != null)
            {
                control.Sync += new EventHandler<SyncEventArgs>(OnSyncControl);
                control.SynchronizeWith(this);
            }
        }

        void ISynchronizable.SynchronizeWith(ISynchronizable control)
        {
            control.Sync += OnSyncControl;
        }

        protected virtual void OnSyncControl(object sender, SyncEventArgs e)
        {
            switch (e.Reason)
            {
                case SynEventReason.NavigateTo: SyncNavigateTo(e.Node, e.Offset); break;
                case SynEventReason.Pan: SyncPan(e.Node, e.Offset); break;
                case SynEventReason.Select: Offset = e.Offset; SelectedNode = e.Node; break;
            }
        }

        private void SyncPan(INavigationNode node, float offset)
        {
            this.SelectedNode = node;
            this.Offset = offset;
        }

        protected virtual void SyncNavigateTo(INavigationNode node, float offset)
        {
            this.SelectedNode = node;
            Offset = offset;
            ScrollToOffset(0f, scrollDuration);
        }


        /// <summary>
        /// Occurs when the Node property changes.
        /// </summary>
        /// <param name="oldValue">Old value of Node property.</param>
        protected virtual void OnSelectedNodeChanged(INavigationNode oldValue)
        {
            Invalidate(InvalidationFlag.Appearance);
            OnSync(selectedNode);
            if (NodeChanged != null)
            {
                NodeChangedEventArgs e = new NodeChangedEventArgs(oldValue, selectedNode);
                NodeChanged(this, e);
            }
        }

        protected override void EnsureRestStateOnSizeChanged()
        {

        }

        public virtual void NavigateToNext()
        {
            var next = selectedNode != null ? selectedNode.Next : null;
            if (next != null)
            {
                float offset = this.offset + 1f;
                NavigateTo(next, offset);
            }
        }

        public virtual void NavigateToPrevious()
        {
            var previous = selectedNode != null ? selectedNode.Previous : null;
            if (previous != null)
            {
                float offset = this.offset - 1f;
                NavigateTo(previous, offset);
            }
        }

        protected void NavigateTo(INavigationNode node, float offset)
        {
            this.SelectedNode = node;
            Offset = offset;
            OnSync(node, offset, SynEventReason.NavigateTo);
            ScrollToOffset(0f, scrollDuration);
        }

        /// <summary>
        /// Selects a node.
        /// </summary>
        /// <param name="node">Node to select.</param>
        /// <param name="animated">true to select animated, otherwise false.</param>
        public virtual void SelectNode(INavigationNode node, bool animated)
        {
            if (node == null || !animated && !IsInitialized)
            {
                this.SelectedNode = node;
                this.Offset = 0;
            }
            else
            {
                var current = this.selectedNode;
                if (current.Next == node) NavigateToNext();
                else if (current.Previous == node) NavigateToPrevious();
                else
                {
                    this.SelectedNode = node;
                    this.Offset = 0;
                }
            }
        }

        private INavigationNode touchedNode;

        /// <summary>
        /// Occurs when the display is being touched.
        /// </summary>
        /// <param name="e">TouchEventArgs.</param>
        protected internal override void OnTouchDown(TouchEventArgs e)
        {
            allowFlick = true;
            touchedNode = SelectedNode;
            base.OnTouchDown(e);
        }

        /// <summary>
        /// Occurs when the control is about to receive OnPan events.
        /// This can be accepted by setting e.Handled to true, and aborted for all other controls by setting e.Cancel to true.
        /// This method is also the place to prepare for swiping, e.g. making a scrollbar visible.
        /// Set e.Value to a value that is the current value to be scrolled. e.g. for a scrollbar which scrolls YOffset property, e.Value would be set to new Point(0,YOffset).
        /// Set e.Handled to true to receive events at OnScroll method.
        /// </summary>
        /// <param name="e">ScrollEventArgs.</param>
        protected internal override void OnBeginPan(PanEventArgs e)
        {
            if (!e.Handled)
            {
                if (e.HorizontalMotion && AllowDrag)
                {
                    e.Handled = true;
                    e.Value = new Point((int)(offset * ScrollWidth), 0);
                }
            }
            base.OnBeginPan(e);
        }

        protected internal override void OnEndPan(EventArgs e)
        {
            if (!OffsetProperty.IsAnimated(this)) EnsureRestState(true);
            base.OnEndPan(e);
        }

        protected internal override void OnFlick(TouchEventArgs e)
        {
            if (!e.Handled && e.HorizontalMotion && AllowDrag && allowFlick)
            {
                if (touchedNode != null && e.Velocity >= TouchEventArgs.LongFlickVelocity)
                {
                    switch (e.Direction)
                    {
                        case PanDirection.Left:
                            NavigateToNext();
                            e.Handled = true;
                            break;

                        case PanDirection.Right:
                            NavigateToPrevious();
                            e.Handled = true;
                            break;
                    }
                }
            }
            base.OnFlick(e);
        }

        internal protected override void OnPan(PanEventArgs e)
        {
            if (!e.Handled)
            {
                PanInternal(e);
            }
            base.OnPan(e);
        }

        protected virtual void PanInternal(PanEventArgs e)
        {
            if (e.HorizontalMotion)
            {
                int value = e.Value.X;
                float offset = (float)value / ScrollWidth;
                if (offset > 0f && (selectedNode == null || selectedNode.Previous == null)) offset = 0f;
                else if (offset < 0f && (selectedNode == null || selectedNode.Next == null)) offset = 0f;
                Offset = offset;
                OnSync(selectedNode, offset, SynEventReason.Pan);
                e.Handled = true;
                if (offset > 0.7f || offset < -0.65f)
                {
                    allowFlick = false;
                    e.Cancel = true;
                    if (offset < 0) NavigateToNext(); else NavigateToPrevious();
                }

            }
        }

        /// <summary>
        /// Sets all properties to a defined value.
        /// If overridden, this method determines the ideal values of specific properties, for instance, when a property is changed as the
        /// uses swipes between possible values.
        /// It's best practice to access each property value  using PropertyAccessor.GetUnanimatedValue.
        /// </summary>
        /// <param name="animated">True when to allowed to tranistion to a new value, otherwise false.</param>
        public override void EnsureRestState(bool animated)
        {
            NavigationBase.OffsetProperty.StopAnimation(this);
            if (offset != 0f)
            {
                if (animated) NavigateTo(selectedNode, offset);
                else
                {
                    Offset = 0f;
                    OnSync(selectedNode, 0f, SynEventReason.Select);
                    OnSync(selectedNode);
                }
            }
            base.EnsureRestState(animated);
        }

        protected void ScrollToOffset(float offset, int duration)
        {
            FloatTransition transition = new FloatTransition
            {
                Duration = duration,
                Property = NavigationBase.OffsetProperty,
                //EasingFunction = PowerEase.Out,
                EasingFunction = scrollEase,
                Tracking = TrackMode.Keep,
                To = offset
            };
            transition.Completed += new EventHandler(OnNavigated);
            IsScrolling = true;
            NavigationBase.OffsetProperty.BeginAnimation(this, transition);
        }

        protected virtual void OnNavigated(object sender, EventArgs e)
        {
            IsScrolling = false;
            RaiseEvent(NavigatedEvent);
        }

        protected void OnTileTap(Tile tile)
        {

            if (tile == null) throw new ArgumentNullException("tile");
            int delta = tile.Delta;
            if (delta != 0)
            {
                INavigationNode node = GetNodeFromDelta(delta);
                if (node != null)
                {
                    NavigateTo(node, delta * 1f);
                }
            }
        }

        private INavigationNode GetNodeFromDelta(int delta)
        {
            INavigationNode node = this.selectedNode;
            while (delta > 0 && node != null) { node = node.Next; delta--; }
            while (delta < 0 && node != null) { node = node.Previous; delta++; }
            return node;
        }

        protected static void InitializeControl(Control c)
        {
            if (c != null && c.CheckFlag(InvalidationFlag.NoStyleApplied)) c.PreInitialize();
        }

        public void GoBack(bool animated)
        {
            INavigationNode node = this.SelectedNode;
            if (node != null) node = node.Previous;
            if (node != null) SelectNode(node, animated);
        }

        public void GoForward(bool animated)
        {
            INavigationNode node = this.SelectedNode;
            if (node != null) node = node.Next;
            if (node != null) SelectNode(node, animated);
        }

        protected virtual void OnSync(INavigationNode node, float offset, SynEventReason reason)
        {
            if (Sync != null) Sync(this, new SyncEventArgs(node, offset, reason));
        }

        protected virtual void OnSync(INavigationNode node)
        {
            if (Sync != null) Sync(this, new SyncEventArgs(node, Offset, SynEventReason.Select));
        }


        #endregion
        #region events

        /// <summary>
        /// Occurs when <see cref="IsScrolling"/> value changes.
        /// </summary>
        public static SparseEvent ScrollingChangedEvent = new SparseEvent();

        public static SparseEvent NavigatedEvent = new SparseEvent();

        /// <summary>
        /// Occurs when <see cref="IsScrolling"/> value changes.
        /// </summary>
        public event EventHandler ScrollingChanged
        {
            add { AddHandler(ScrollingChangedEvent, value); }
            remove { RemoveHandler(ScrollingChangedEvent, value); }
        }

        public event EventHandler OffsetChanged;

        /// <summary>
        /// Occurs when Offset is set to 0.
        /// </summary>
        public event EventHandler<NodeChangedEventArgs> NodeChanged;

        /// <summary>
        /// Occurs when to get or create a control for a navigation node.
        /// </summary>
        public event EventHandler<NavigationEventArgs> GetControl;

        /// <summary>
        /// Releases a control that displays a specific node.
        /// </summary>
        /// <example>
        /// A method could dispose the control that was created to display the specified node.
        /// </example>
        public event EventHandler<NavigationEventArgs> ReleaseControl;

        /// <summary>
        /// Occurs when animated navigation to a new node has completed.
        /// </summary>
        public event EventHandler Navigated
        {
            add { AddHandler(NavigatedEvent, value); }
            remove { RemoveHandler(NavigatedEvent, value); }
        }

        /// <summary>
        /// Occurs when the navigational control needs to be syncronized with another.
        /// </summary>
        public event EventHandler<SyncEventArgs> Sync;

        #endregion    }
    }
}
